package core

import (
	"fmt"
	"net/http"
	"sort"
	"strings"
	"time"

	"golang.org/x/sync/errgroup"

	"github.com/goccy/go-json"

	"codeberg.org/vnpower/pixivfe/v2/audit"

	"codeberg.org/vnpower/pixivfe/v2/i18n"
	"codeberg.org/vnpower/pixivfe/v2/server/session"
)

var artworkCommentService = NewCommentService(GetArtworkCommentsURL)

// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `XRestrict`
// 0: Safe
// 1: R18
// 2: R18G
type XRestrict int

const (
	Safe XRestrict = 0
	R18  XRestrict = 1
	R18G XRestrict = 2
)

func (x XRestrict) String() (string, error) {
	switch x {
	case Safe:
		return i18n.Tr("Safe"), nil
	case R18:
		return i18n.Tr("R18"), nil
	case R18G:
		return i18n.Tr("R18G"), nil
	}
	return "", fmt.Errorf("invalid value: %#v", int(x))
}

// Pixiv returns 0, 1, 2 to filter SFW and/or NSFW artworks.
// Those values are saved in `aiType`
// 0: Not rated / Unknown
// 1: Not AI-generated
// 2: AI-generated

type AiType int

const (
	Unrated AiType = 0
	NotAI   AiType = 1
	AI      AiType = 2
)

func (x AiType) String() (string, error) {
	switch x {
	case Unrated:
		return i18n.Tr("Unrated"), nil
	case NotAI:
		return i18n.Tr("Not AI"), nil
	case AI:
		return i18n.Tr("AI"), nil
	}
	return "", fmt.Errorf("invalid value: %#v", int(x))
}

type ImageResponse struct {
	Width  int               `json:"width"`
	Height int               `json:"height"`
	Urls   map[string]string `json:"urls"`
}

type Image struct {
	Width      int
	Height     int
	Small      string
	Medium     string
	Large      string
	Original   string
	IllustType int
}

type ArtworkTag struct {
	Name           string `json:"tag"`
	TranslatedName string `json:"translation"`
}

type Comment struct {
	AuthorID   string `json:"userId"`
	AuthorName string `json:"userName"`
	Avatar     string `json:"img"`
	Context    string `json:"comment"`
	Stamp      string `json:"stampId"`
	Date       string `json:"commentDate"`
}

type UserBrief struct {
	ID     string `json:"userId"`
	Name   string `json:"name"`
	Avatar string `json:"imageBig"`
}

type ArtworkBrief struct {
	ID           string `json:"id"`
	Title        string `json:"title"`
	ArtistID     string `json:"userId"`
	ArtistName   string `json:"userName"`
	ArtistAvatar string `json:"profileImageUrl"`
	Thumbnail    string `json:"url"`
	Thumbnails   Thumbnails
	Pages        int      `json:"pageCount"`
	XRestrict    int      `json:"xRestrict"`
	AiType       int      `json:"aiType"`
	Bookmarked   any      `json:"bookmarkData"`
	IllustType   int      `json:"illustType"`
	Tags         []string `json:"tags"` // used by core/popular_search
}

type Illust struct {
	ID              string    `json:"id"`
	Title           string    `json:"title"`
	Description     HTML      `json:"description"`
	UserID          string    `json:"userId"`
	UserName        string    `json:"userName"`
	UserAccount     string    `json:"userAccount"`
	Date            time.Time `json:"uploadDate"`
	Images          []Image
	Tags            []ArtworkTag `json:"tags"`
	Pages           int          `json:"pageCount"`
	Bookmarks       int          `json:"bookmarkCount"`
	Likes           int          `json:"likeCount"`
	Comments        int          `json:"commentCount"`
	Views           int          `json:"viewCount"`
	CommentDisabled int          `json:"commentOff"`
	SanityLevel     int          `json:"sl"`
	XRestrict       XRestrict    `json:"xRestrict"`
	AiType          AiType       `json:"aiType"`
	BookmarkData    any          `json:"bookmarkData"`
	Liked           bool         `json:"likeData"`
	SeriesNavData   struct {
		SeriesType  string `json:"seriesType"`
		SeriesID    string `json:"seriesId"`
		Title       string `json:"title"`
		IsWatched   bool   `json:"isWatched"`
		IsNotifying bool   `json:"isNotifying"`
		Order       int    `json:"order"`
		Next        struct {
			Title string `json:"title"`
			Order int    `json:"order"`
			ID    string `json:"id"`
		} `json:"next"`
		Prev struct {
			Title string `json:"title"`
			Order int    `json:"order"`
			ID    string `json:"id"`
		} `json:"prev"`
	} `json:"seriesNavData"`
	User         UserBrief
	RecentWorks  []ArtworkBrief
	RelatedWorks []ArtworkBrief
	CommentsList []Comment
	IsUgoira     bool
	BookmarkID   string
	IllustType   int `json:"illustType"`
}

func GetUserBasicInformation(auditor *audit.Auditor, r *http.Request, id string) (UserBrief, error) {
	var user UserBrief

	URL := GetUserInformationURL(id)

	resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
	if err != nil {
		return user, err
	}
	resp, err = session.ProxyImageUrl(r, resp)
	if err != nil {
		return user, err
	}

	err = json.Unmarshal([]byte(resp), &user)
	if err != nil {
		return user, err
	}

	return user, nil
}

func GetArtworkImages(auditor *audit.Auditor, r *http.Request, id string, illustType int) ([]Image, error) {
	var imageResp []ImageResponse
	var images []Image

	URL := GetArtworkImagesURL(id)

	resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
	if err != nil {
		return nil, err
	}
	resp, err = session.ProxyImageUrl(r, resp)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal([]byte(resp), &imageResp)
	if err != nil {
		return images, err
	}

	// Extract and proxy every images
	for _, imageRaw := range imageResp {
		var image Image

		// this is the original art dimention, not the "regular" art dimension
		// the image ratio of "regular" is close to Width/Height
		// maybe not useful
		image.Width = imageRaw.Width
		image.Height = imageRaw.Height

		image.Small = imageRaw.Urls["thumb_mini"]
		image.Medium = imageRaw.Urls["small"]
		image.Large = imageRaw.Urls["regular"]
		image.Original = imageRaw.Urls["original"]

		// Required for logic to display manga differently
		image.IllustType = illustType

		// Debug statement
		// log.Printf("Artwork ID: %s, IllustType set to %d", id, image.IllustType)

		images = append(images, image)
	}

	return images, nil
}

func GetRelatedArtworks(auditor *audit.Auditor, r *http.Request, id string) ([]ArtworkBrief, error) {
	var body struct {
		Illusts []ArtworkBrief `json:"illusts"`
	}

	// TODO: keep the hard-coded limit?
	URL := GetArtworkRelatedURL(id, 180)

	resp, err := API_GET_UnwrapJson(r.Context(), auditor, URL, "", r.Header)
	if err != nil {
		return nil, err
	}

	resp, err = session.ProxyImageUrl(r, resp)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal([]byte(resp), &body)
	if err != nil {
		return nil, err
	}

	// Populate thumbnails for each artwork
	for id, artwork := range body.Illusts {
		if err := artwork.PopulateThumbnails(auditor); err != nil {
			auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err)
			return nil, fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err)
		}
		body.Illusts[id] = artwork
	}

	return body.Illusts, nil
}

// GetArtworkByID retrieves information about a specific artwork, with the option to include additional related data.
func GetArtworkByID(auditor *audit.Auditor, r *http.Request, id string, full bool) (*Illust, error) {
	token := session.GetUserToken(r)

	// Core artwork metadata fetched synchronously.
	var illust struct {
		Illust
		UserIllusts map[int]any     `json:"userIllusts"` // User's other artworks.
		RawTags     json.RawMessage `json:"tags"`        // Untranslated tags.
	}

	// Fetch core artwork metadata.
	urlArtInfo := GetArtworkInformationURL(id)
	response, err := API_GET_UnwrapJson(r.Context(), auditor, urlArtInfo, token, r.Header)
	if err != nil {
		return nil, err
	}

	// Decode basic artwork data.
	err = json.Unmarshal([]byte(response), &illust)
	if err != nil {
		return nil, err
	}

	// If the artwork is bookmarked, store its BookmarkID.
	if illust.BookmarkData != nil {
		t, ok := illust.BookmarkData.(map[string]any)
		if ok && t["id"] != nil {
			illust.BookmarkID = t["id"].(string)
		}
	}

	// Use an errgroup to manage goroutines and errors.
	var g errgroup.Group

	// Fetch user basic information.
	g.Go(func() error {
		userInfo, err := GetUserBasicInformation(auditor, r, illust.UserID)
		if err != nil {
			return err
		}
		illust.User = userInfo
		return nil
	})

	// Fetch artwork images.
	g.Go(func() error {
		images, err := GetArtworkImages(auditor, r, id, illust.IllustType)
		if err != nil {
			return err
		}
		illust.Images = images
		return nil
	})

	// Fetch translated tags.
	g.Go(func() error {
		var tags struct {
			Tags []struct {
				Tag         string            `json:"tag"`
				Translation map[string]string `json:"translation"`
			} `json:"tags"`
		}
		err := json.Unmarshal(illust.RawTags, &tags)
		if err != nil {
			return err
		}
		// Store translated tags.
		var tagsList []ArtworkTag
		for _, tag := range tags.Tags {
			newTag := ArtworkTag{
				Name:           tag.Tag,
				TranslatedName: tag.Translation["en"],
			}
			tagsList = append(tagsList, newTag)
		}
		illust.Tags = tagsList
		return nil
	})

	// Conditionally fetch related artworks if 'full' is true.
	if full {
		// Fetch related artworks.
		g.Go(func() error {
			related, err := GetRelatedArtworks(auditor, r, id)
			if err != nil {
				return err
			}
			illust.RelatedWorks = related
			return nil
		})

		// Fetch recent works.
		g.Go(func() error {
			ids := make([]int, 0, len(illust.UserIllusts))
			for k := range illust.UserIllusts {
				ids = append(ids, k)
			}
			sort.Sort(sort.Reverse(sort.IntSlice(ids)))
			idsString := ""
			count := min(len(ids), 20)
			for i := 0; i < count; i++ {
				idsString += fmt.Sprintf("&ids[]=%d", ids[i])
			}
			recent, err := fetchArtworkIDs(auditor, r, illust.UserID, idsString)
			if err != nil {
				return err
			}
			sort.Slice(recent[:], func(i, j int) bool {
				return numberGreaterThan(recent[i].ID, recent[j].ID)
			})

			// Populate thumbnails for each artwork
			for id, artwork := range recent {
				if err := artwork.PopulateThumbnails(auditor); err != nil {
					auditor.SugaredLogger.Errorf("Failed to populate thumbnails for artwork ID %s: %v", id, err)
					return fmt.Errorf("failed to populate thumbnails for artwork ID %s: %w", id, err)
				}
				recent[id] = artwork
			}

			illust.RecentWorks = recent
			return nil
		})

		// Fetch comments if comments are enabled.
		if illust.CommentDisabled != 1 {
			g.Go(func() error {
				comments, err := artworkCommentService.GetComments(auditor, r, id)
				if err != nil {
					return err
				}
				illust.CommentsList = comments
				return nil
			})
		}
	}

	// Wait for all goroutines to finish.
	if err := g.Wait(); err != nil {
		return nil, err
	}

	// Check if the artwork is a ugoira.
	illust.IsUgoira = strings.Contains(illust.Images[0].Original, "ugoira")

	// Return the fully assembled 'illust' object.
	return &illust.Illust, nil
}
